package se.sll.snod;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.net.ProtocolException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class SnodClient {

    private static final Logger log = Logger.getLogger(SnodClient.class.getName());
    private final String defaultMethod = "POST";
    private final String defaultPrefix = "/snod";
    private final String defaultUserAgent = "Snod/2.5.0";
    private final String defaultUrlCharEncoding = "UTF-8";
    private String baseUrl;
    private String username;
    private String service;
    private String password;
    private String oldExt = "old";
    private boolean serviceStatus = true;
    private boolean accessStatus = false;
    private HostnameVerifier verifier;
    private HttpsURLConnection connection;

    /**
     * Creates a new <code>SnodClient</code> instance implementing ssl.
     *
     * @param host the snod server host
     * @param port https port of the snod server, normally 10443
     * @param service the snod service
     * @param username snod user name
     * @param password snod password
     * @exception SnodException if an error occurs
     * @see javax.net.ssl.HttpsURLConnection
     */
    public SnodClient(String host, String port, java.lang.String service, java.lang.String username, java.lang.String password) throws SnodException {
        try {
            SnodClient.trustHttpsCertificates();
            baseUrl = "https://" + host + ":" + port + defaultPrefix;
            this.username = username;
            this.password = password;
            this.service = service;
            verifier = new MyVerifier();
            accessStatus = true;
        } catch (Exception e) {
            System.out.println("Java Exception while initializing " + service);
            serviceStatus = false;
        }
    }

    /**
     * Query selected service. Any exceptions will result in false result.<br>
     *
     * @param arg a <code>String</code> value, i.e. "191212121212".
     * @return a <code>boolean</code> value, false if exceptions where
     * encountered.
     */
    public boolean queryService(java.lang.String arg) {
        if (serviceStatus) { //by default serviceStatus is true if init threw error it is false
            serviceStatus = true;
            accessStatus = true;
            try {
                request(buildGenericUrl(arg, ""), defaultMethod);
            } catch (SnodException se) {
                System.out.println("Java SnodException while accessing " + service);
                serviceStatus = false;
            } catch (ProtocolException pe) {
                System.out.println("Java ProtocolException while accessing " + service);
                serviceStatus = false;
            } catch (IOException ioe) {
                System.out.println("Java IOException while accessing " + service);
                serviceStatus = false;
            } catch (Exception e) {
                System.out.println("Java Exception while accessing " + service);
                serviceStatus = false;
            }
        }
        return serviceStatus;
    }

    /**
     * Accesses result of <a
     * href="#queryService(java.lang.String)">queryService</a> and returns array
     * of string as result.
     *
     *
     * @return a <code>String[]</code> value
     *
     */
    public java.lang.String getResult() {
        StringBuffer result = null;
        java.lang.String resultstring = "";

        try {
            if (!accessStatus || !serviceStatus) {
                System.out.println("Java Exception while getting result from " + service);
                System.out.println("Service error or not accessed.");
            } else {
                result = getSimpleResponse();
            }
        } catch (Exception e) {
            System.out.println("Java Exception while getting result from " + service);
            e.printStackTrace();
        } finally {
            return result.toString();
        }
    }

    /**
     * Sets the extension used for renaming files on the server.
     *
     * @param ext - the extension
     *
     * private for now /Mattias
     *
     */
    private void setOldExt(String ext) {
        oldExt = ext;
    }

    /**
     * Sets the hostname verifier. The default verifier ignores the hostname.
     *
     * @param verifier the HostnameVerifier.
     *
     * private for now /Mattias
     *
     */
    private void setHostnameVerifier(HostnameVerifier verifier) {
        this.verifier = verifier;
    }

    /**
     * Ping selected server.
     *
     * @return a <code>boolean</code> true if contact (PING) with server can be
     * achieved.
     */
    public boolean ping() {
        try {
            request("/PING", defaultMethod);
            httpRequestCheck();
        } catch (Exception e) {
            log.info("Ping: " + e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * Gets all handlers available in the server.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/PINGHANDLERS"
     *
     * @return a <code>List</code> value An array of Strings with the handlers
     * support through this client.
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private StringBuffer pingHandlers() throws SnodException, ProtocolException, IOException {
        final int handlerField = 3;
        request("/PINGHANDLERS", defaultMethod);
        return getSimpleResponse();
    }

    /**
     * Gets information about the handler.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/PINGINFO?arg=<service>"
     *
     * @param handler name of the handler.
     * @return a <code>String</code> value the info associated with this
     * handler.
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs * private for now /Mattias
     *
     */
    private StringBuffer pingInfo(String handler) throws SnodException, ProtocolException, IOException {
        request("/PINGINFO?arg=" + URLEncoder.encode(handler, defaultUrlCharEncoding), defaultMethod);
        return getSimpleResponse();
    }

    /**
     * Gets the specified file from the server and save it with the same name
     * locally.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?extra=-o+<filename>"
     *
     * @param name a <code>String</code> value
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void getFile(String name) throws SnodException, ProtocolException, IOException {
        request("/" + service + "?extra=-o+" + URLEncoder.encode(name, defaultUrlCharEncoding), defaultMethod);
        httpRequestCheck();
        FileOutputStream fos = new FileOutputStream(name);
        copyStream(connection.getInputStream(), fos);
        fos.close();
    }

    /**
     * <code>getFile</code> get the specified file from the server.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?extra=-o+<filename>"
     *
     * @param name name on the server
     * @param outname name locally
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void getFile(String name, String outname) throws SnodException, ProtocolException, IOException {
        request("/" + service + "?extra=-o+" + URLEncoder.encode(name, defaultUrlCharEncoding), defaultMethod);
        httpRequestCheck();
        FileOutputStream fos = new FileOutputStream(outname);
        copyStream(connection.getInputStream(), fos);
        fos.close();
    }

    /**
     * Adds the old-extension to the file name on the server. Only available
     * with a service that can receive files. See {@link #setOldExt setOldExt}
     * for how to set the extension.
     *
     * <p>
     * Request:
     * "https://127.0.0.1:443/snod/<service>?extra=-o+<filename>+-old=<extension>"
     *
     * @param name The file name on the server.
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void setOld(String name) throws SnodException, ProtocolException, IOException {
        //assert (oldExt != null);
        request("/" + service
                + "?extra=-o+" + URLEncoder.encode(name, defaultUrlCharEncoding)
                + "+-old=" + URLEncoder.encode(oldExt, defaultUrlCharEncoding),
                defaultMethod);
        httpRequestCheck();
    }

    /**
     * Moves away the file to a (pre)configured folder on the server. Use with a
     * service that can receive files.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?extra=-move=<name>"
     *
     * @param name The file name on the server
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void move(String name) throws SnodException, ProtocolException, IOException {
        request("/" + service + "?extra=-move+" + URLEncoder.encode(name, defaultUrlCharEncoding), defaultMethod);
        httpRequestCheck();
    }

    /**
     * Adds extension .err to the filename on the server. Use with a service
     * that send files.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?extra=-rename=<name>"
     *
     * @param name The file name on the server
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void rename(String name) throws SnodException, ProtocolException, IOException {
        request("/" + service + "?extra=-rename+" + URLEncoder.encode(name, defaultUrlCharEncoding), defaultMethod);
        httpRequestCheck();
    }

    /**
     * Transfers a file to the server. The file is stored with the basename of
     * the file on the server.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?extra=-o+<name>"
     *
     * @param file The File
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void putFile(File file) throws SnodException, ProtocolException, IOException {
        request("/" + service + "?extra=-o+" + URLEncoder.encode(file.getName(), defaultUrlCharEncoding), defaultMethod);
        connection.setRequestProperty("content-length", "" + file.length());
        connection.setRequestProperty("content-type", "application/octet-string");
        FileInputStream fis = new FileInputStream(file);
        copyStream(fis, connection.getOutputStream());
        fis.close();
        log.finer("File transfered: " + file.getName());
        httpRequestCheck();
    }

    /**
     * Transfers the file to the server and gives it a unique name. The
     * generated name is a configuration option for the service on the server.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>"
     *
     * @param file The file
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private void putFileUniq(File file) throws SnodException, ProtocolException, IOException {
        request("/" + service, defaultMethod);
        connection.setRequestProperty("content-length", "" + file.length());
        connection.setRequestProperty("content-type", "application/octet-string");

        log.finer("Starting to send file: " + file.getName());
        FileInputStream fis = new FileInputStream(file);
        copyStream(fis, connection.getOutputStream());
        fis.close();
        log.finer("File transfered: " + file.getName());

        httpRequestCheck();
    }

    /**
     * Gets a list of files on the server.
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?extra=-lista"
     *
     * @return a list of file names. If there are no files the list is empty.
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private StringBuffer getFileList() throws SnodException, ProtocolException, IOException {
        request("/" + service + "?extra=-lista", defaultMethod);
        return getSimpleResponse();
    }

    /**
     * Generic section. Support for yet unknown SNOD-applications. These methods
     * provide a generic interface where user specifies the arg and extra
     * parameters of the SNOD-url themselves.
     */
    /**
     * Builds the generic URLs for the generic support. The pattern is:
     *
     * <p>
     * Request: "https://127.0.0.1:443/snod/<service>?arg=arg+extra=extra"
     *
     * @param arg a <code>String</code> value
     * @param extra a <code>String</code> value
     * @return a <code>String</code> value
     * @exception IOException if an error occurs
     */
    private String buildGenericUrl(String arg, String extra) throws IOException {
        String result = "/" + service;
        if (arg.length() != 0 || extra.length() != 0) {
            result += "?";
            if (arg.length() != 0) {
                result += "arg=" + URLEncoder.encode(arg, defaultUrlCharEncoding);
                if (extra.length() != 0) {
                    result += "+";
                }
            }
            if (extra.length() != 0) {
                result += "extra=" + URLEncoder.encode(extra, defaultUrlCharEncoding);
            }
        }
        return result;
    }

    /**
     * Make server do something and ignore results except throught exceptions.
     *
     * @param arg a <code>String</code> value
     * @param extra a <code>String</code> value
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias *
     */
    private void doGeneric(String arg, String extra) throws SnodException, ProtocolException, IOException {
        request(buildGenericUrl(arg, extra), defaultMethod);
        httpRequestCheck();
    }

    /**
     * Do something where a list of <code>Strings</code> as return makes sence.
     *
     * @param arg a <code>String</code> value
     * @param extra a <code>String</code> value
     * @return a <code>String[]</code> value
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private StringBuffer getGeneric(String arg, String extra) throws SnodException, ProtocolException, IOException {
        request(buildGenericUrl(arg, extra), defaultMethod);
        return getSimpleResponse();
    }

    /**
     * Do something where getting an InputStream connected to the server makes
     * sence.
     *
     * <p>
     * Could be creating a file on the fly and send it to the server.
     *
     * @param arg a <code>String</code> value
     * @param extra a <code>String</code> value
     * @return an <code>InputStream</code> value
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     *
     * private for now /Mattias
     *
     */
    private InputStream getInputStream(String arg, String extra) throws SnodException, ProtocolException, IOException {
        request(buildGenericUrl(arg, extra), defaultMethod);
        httpRequestCheck();
        return connection.getInputStream();
    }

    /**
     * Verifyer that accepts all hostnames.
     *
     */
    private class MyVerifier implements HostnameVerifier {

        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

    /**
     * Constructs a request header common for all requests.
     *
     */
    private void setRequestHeader() {
        connection.setRequestProperty("Authorization",
                "BASIC "
                + new sun.misc.BASE64Encoder().encode((username
                        + ":"
                        + password).getBytes()));
        connection.setRequestProperty("User-Agent", defaultUserAgent);
        connection.setRequestProperty("Pragma", "no-cache");
        connection.setRequestProperty("Accept", "text/plain, text/html");
    }

    /**
     * Performs a common request.
     *
     * @param url a <code>String</code> value
     * @param method a <code>String</code> value
     * @exception SnodException if an error occurs
     */
    private void request(String url, String method) throws SnodException {
        try {
            URL u = new URL(baseUrl + url);
            connection = (HttpsURLConnection) u.openConnection();
            connection.setHostnameVerifier(verifier);
            setRequestHeader();
            connection.setRequestMethod(method);
            connection.setDoOutput(true);
        } catch (Exception e) {
            throw new SnodException(e.getMessage());
        }
    }

    /**
     * Checks the request for errors both at the HTTP and the SNOD level.
     *
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     */
    private void httpRequestCheck() throws SnodException, ProtocolException, IOException {
        System.out.println("connection: " + connection);
        if (connection.HTTP_OK != connection.getResponseCode()) {
            throw new SnodException("HTTP ERROR: " + connection.getResponseCode());
        }
        String snodReturn = connection.getHeaderField("OutgoingHeader");
        int returnCode = Integer.parseInt(snodReturn.substring(0, snodReturn.indexOf(" ")));
        if (returnCode < 0) {
            throw new SnodException("SNOD ERROR: " + returnCode);
        }
    }

    /**
     * Returns the output from the server as an array. Useful for handling
     * simple types of responses from the server.
     *
     * @return a string array with the lines from the server.
     * @exception SnodException if an error occurs
     * @exception ProtocolException if an error occurs
     * @exception IOException if an error occurs
     */
    private StringBuffer getSimpleResponse() throws SnodException, ProtocolException, IOException {
        StringBuffer result = new StringBuffer();
        httpRequestCheck();
        BufferedReader d = new BufferedReader(new InputStreamReader(connection.getInputStream(), "ISO-8859-1"));
        String dl;
        while ((dl = d.readLine()) != null) {
            result.append(dl + System.getProperty("line.separator"));
        }
        return result;
    }

    /**
     * Splits the string into fields. Useful for handling replies form the
     * server on the form:
     *
     * <p>
     * ignored text: field1, field2, filed3, ...
     *
     * @param str a <code>String</code> value
     * @return a <code>List</code> of fields
     */
    private List getFields(String str) {
        String[] f = str.substring(str.indexOf(":") + 1).split(",");
        ArrayList result = new ArrayList();
        for (String f1 : f) {
            String ft = f1.trim();
            if (0 < ft.length()) {
                result.add(ft);
            }
        }
        return result;
    }

    /**
     * Sets a trust manager that ignores the certificate chains. Use if the Snod
     * server has a certificate that can't be verified.
     *
     * @exception Exception if an error occurs
     *
     * private for now /Mattias
     *
     */
    private static void trustHttpsCertificates() throws Exception {
        Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
        TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs,
                        String authType) throws CertificateException {
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs,
                        String authType) throws CertificateException {
                }
            }
        };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    }

    /**
     * Convenience method for copying a stream.
     *
     * @param in an <code>InputStream</code> value
     * @param out an <code>OutputStream</code> value
     * @exception IOException if an error occurs
     */
    private void copyStream(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[256];
        while (true) {
            int bytesRead = in.read(buffer);
            if (bytesRead == -1) {
                break;
            }
            out.write(buffer, 0, bytesRead);
        }
    }

    /**
     * Returns SnodClient version for debugging purposes.
     *
     * @return
     */
    public java.lang.String version() {
        // Nicklas Johansson/Cybercom Sweden East 2007-04-25: new version 1.1.1 (removed use of ready when reading response)
        return "1.1.1 for jvm 1.4 (with jsse)";
    }
}
